@file:OptIn(ExperimentalTime::class)

package com.gitee.wsl.scheduler.job.runtime

import com.gitee.wsl.scheduler.job.bean.JobExecutionType
import com.gitee.wsl.scheduler.job.bean.JobProps
import com.gitee.wsl.scheduler.job.bean.JobStatus
import com.gitee.wsl.scheduler.job.bean.ScheduledJob
import com.gitee.wsl.scheduler.job.repository.JobRepository
import com.gitee.wsl.logger.Logger
import com.gitee.wsl.logger.debug
import com.gitee.wsl.logger.error
import com.gitee.wsl.logger.getLogger
import com.gitee.wsl.scheduler.job.context.JobContextWithProps
import com.gitee.wsl.time.between
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlin.time.Clock
import kotlin.coroutines.CoroutineContext
import kotlin.time.Duration
import kotlin.time.ExperimentalTime
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid

interface JobExecutor {
    fun execute(runnableJob: RunnableJob, scheduledJob: ScheduledJob, jobRepository: JobRepository)
    fun canExecute(executionType: JobExecutionType): Boolean

    companion object
}

@OptIn(ExperimentalUuidApi::class)
fun JobExecutor.Companion.create(
    kjobId: Uuid = Uuid.random(),
    executors: Map<JobExecutionType, DispatcherWrapper> = emptyMap(),
    clock: Clock = Clock.System,
    coroutineContext: CoroutineContext = Job()
): JobExecutor = DefaultJobExecutor(kjobId, executors,  clock,coroutineContext)


internal class DefaultJobExecutor @OptIn(ExperimentalUuidApi::class) constructor(
    private val kjobId: Uuid,
    private val executors: Map<JobExecutionType, DispatcherWrapper>,
    private val clock: Clock,
    override val coroutineContext: CoroutineContext
) : JobExecutor, CoroutineScope {
    private val logger = Logger.getLogger<DefaultJobExecutor>()

    @OptIn(ExperimentalUuidApi::class)
    override fun execute(runnableJob: RunnableJob, scheduledJob: ScheduledJob, jobRepository: JobRepository) {
        val dispatcher = executors[runnableJob.executionType]?.coroutineDispatcher
            ?: error("Dispatcher not defined for ${runnableJob.executionType}")

        launch(dispatcher + CoroutineName("Job[${scheduledJob.settings.id}]")) {
            scheduledJob.runAt?.let { runAt ->
                val now = clock.now()
                while (now > runAt) {
                    val duration = Duration.between(now, runAt)
                    if (!duration.isNegative()) {
                        delay(duration.inWholeMilliseconds)
                    }
                }
            }
            val isStillMyJob = jobRepository.update(scheduledJob.id, kjobId, kjobId, JobStatus.RUNNING, null, scheduledJob.retries)
            if (!isStillMyJob) {
                return@launch
            }
            val result = try {
                logger.debug("kjob[$kjobId] is executing ${scheduledJob.settings.name}[${scheduledJob.settings.id}]")
                val jobProps = scheduledJob.settings.properties
                runnableJob.execute(
                    JobContextWithProps(
                        coroutineContext,
                        JobProps(jobProps),
                        scheduledJob,
                        jobRepository
                    )
                )
            } catch (e: Exception) {
                logger.error("${scheduledJob.settings.name}[${scheduledJob.settings.id}] failed", e)
                JobError(e)
            }
            withContext(NonCancellable) {
                val (status, message) = when (result) {
                    is JobSuccessful -> JobStatus.COMPLETE to null
                    is JobError ->
                        if (runnableJob.maxRetries <= scheduledJob.retries)
                            JobStatus.FAILED to result.throwable.message
                        else
                            JobStatus.ERROR to result.throwable.message
                }
                jobRepository.update(scheduledJob.id, kjobId, null, status, message, scheduledJob.retries + 1)
            }
        }
    }

    override fun canExecute(executionType: JobExecutionType): Boolean =
        executors[executionType]?.canExecute() ?: error("Executor not defined for $executionType")
}